﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace LevelDB
{
    public class Level
    {
        public const string LEVEL_FILE_SUFFIX = "pilevel";
        private const int LEVEL_HEADER_OFFSET = 3 + sizeof(int);
        private const string LEVEL_MAGIC = "PIL";
        private const int CHUNK_DATA_SIZE = 4096;
        private const int CHUNK_STRIDE = CHUNK_DATA_SIZE + sizeof(int) * 3;

        private static Level _instance;
        public static Level Instance
        {
            get
            {
                if (_instance == null) _instance = new Level();
                return _instance;
            }
        }

        private string levelsDirectory;
        private string levelName;
        private Dictionary<Vector3Int, ChunkHandle> chunks;

        private byte[] buffer;

        private Thread ioThread;
        private bool threadStop;
        private ConcurrentQueue<LevelMessage> messageQueue;

        private Level()
        {
            levelsDirectory = null;
            levelName = null;
            chunks = new Dictionary<Vector3Int, ChunkHandle>();
            messageQueue = new ConcurrentQueue<LevelMessage>();

            buffer = new byte[CHUNK_STRIDE];

            threadStop = false;

            ioThread = new Thread(IOThread);

            ioThread.Start();
        }

        private void IOThread()
        {
            while (!threadStop)
            {
                if (messageQueue.IsEmpty) continue;

                using (FileStream fs = File.Open(LevelPath, FileMode.Open))
                {
                    while (!messageQueue.IsEmpty)
                    {
                        if (messageQueue.TryDequeue(out LevelMessage message))
                        {
                            switch (message.type)
                            {
                                case LevelMessage.MessageType.WriteChunk:
                                    {
                                        if (message.chunk.situation != ChunkHandle.Situation.InDisk)
                                        {
                                            if (message.chunk.fileOffset is long offset)
                                            {
                                                fs.Seek(offset, SeekOrigin.Begin);
                                            }
                                            else
                                            {
                                                fs.Seek(0, SeekOrigin.End);
                                                message.chunk.fileOffset = fs.Position;
                                            }

                                            message.location.GetBytes(buffer);
                                            Buffer.BlockCopy(message.chunk.data, 0, buffer, sizeof(int) * 3, CHUNK_DATA_SIZE);
                                            fs.Write(buffer);
                                            message.chunk.data = null;
                                        }
                                        break;
                                    }

                                case LevelMessage.MessageType.ReadChunk:
                                    {
                                        if (message.chunk.fileOffset is long offset)
                                        {
                                            fs.Seek(offset, SeekOrigin.Begin);
                                        }
                                        else
                                        {
                                            throw new Exception("Unable to read an in-memory chunk!");
                                        }
                                        fs.Read(buffer, 0, CHUNK_STRIDE);

                                        var location = Vector3Int.FromBytes(buffer, 0);
                                        if (!location.Equals(message.location)) throw new Exception("Wrong file offset");

                                        byte[,,] data = new byte[16, 16, 16];
                                        Buffer.BlockCopy(buffer, sizeof(int) * 3, data, 0, CHUNK_DATA_SIZE);
                                        message.chunk.data = data;
                                        break;
                                    }
                            }

                            message.Dispose();
                        }
                    }
                }
            }
        }

        private string LevelPath => (string.IsNullOrEmpty(levelsDirectory) ? levelsDirectory : levelsDirectory + "/") + levelName + "." + LEVEL_FILE_SUFFIX;

        public void SetLevelDirectory(string dir)
        {
            levelsDirectory = dir;
        }

        public void CreateLevel(LevelInfo info)
        {
            if (levelsDirectory == null) throw new Exception("Level directory not set!");
            string filePath = (string.IsNullOrEmpty(levelsDirectory) ? levelsDirectory : levelsDirectory + "/") + info.name + "." + LEVEL_FILE_SUFFIX;
            if (File.Exists(filePath)) File.Delete(filePath);

            ClearState();

            using var fs = File.Create(filePath);

            var magic = Encoding.ASCII.GetBytes("PIL");
            var version = BitConverter.GetBytes(info.version);
            var name = Encoding.UTF8.GetBytes(info.name);
            var numChunks = BitConverter.GetBytes(chunks.Count);

            int len = name.Length + numChunks.Length;

            fs.Write(magic);
            fs.Write(version);
            fs.Write(BitConverter.GetBytes(len));
            fs.Write(name);
            fs.Write(numChunks);

            levelName = info.name;
        }

        public void LoadLevel(string levelName)
        {
            if (levelsDirectory == null) throw new Exception("Level directory not set!");
            ClearState();
            string filePath = (string.IsNullOrEmpty(levelsDirectory) ? levelsDirectory : levelsDirectory + "/") + levelName + "." + LEVEL_FILE_SUFFIX;

            using var fs = File.OpenRead(filePath);

            fs.Read(buffer, 0, 3);
            var magic = Encoding.ASCII.GetString(buffer, 0, 3);
            if (magic != LEVEL_MAGIC) throw new Exception("Not a level");


            var nameOffset = sizeof(int) + sizeof(int);
            fs.Read(buffer, 0, nameOffset);
            var version = BitConverter.ToInt32(buffer, 0);
            var len = BitConverter.ToInt32(buffer, sizeof(int));

            fs.Read(buffer, 0, len);
            var name = Encoding.UTF8.GetString(buffer, 0, len - sizeof(int));
            var chunkCount = BitConverter.ToInt32(buffer, len - sizeof(int));

            Console.WriteLine($"Reading level: {name}\nversion: {version}\nchunkSaved: {chunkCount}");

            for (int i = 0; i < chunkCount; i++)
            {
                var offset = fs.Position;
                fs.Read(buffer, 0, sizeof(int) * 3);
                var chunkLoc = Vector3Int.FromBytes(buffer, 0);
                chunks.Add(chunkLoc, new ChunkHandle(offset));

                fs.Seek(CHUNK_DATA_SIZE, SeekOrigin.Current);
            }

            this.levelName = levelName;
        }

        public string CurrentLevelName => levelName;
        public int ChunkCount => chunks.Count;

        public ChunkHandle GetChunkDataAsync(int x, int y, int z)
        {
            return GetChunkDataAsync(new Vector3Int(x, y, z));
        }

        public ChunkHandle GetChunkDataAsync(Vector3Int chunkLoc)
        {
            if (chunks.TryGetValue(chunkLoc, out ChunkHandle handle))
            {
                if (handle.situation == ChunkHandle.Situation.InDisk)
                {
                    var msg = new LevelMessage();
                    msg.type = LevelMessage.MessageType.ReadChunk;
                    msg.location = chunkLoc;
                    msg.chunk = handle;
                    messageQueue.Enqueue(msg);
                }
                return handle.MakeRef();
            }
            else
            {
                chunks.Add(chunkLoc, new ChunkHandle(new byte[16, 16, 16]));
                return chunks[chunkLoc].MakeRef();
            }
        }
        public ChunkHandle GetChunkData(int x, int y, int z)
        {
            return GetChunkData(new Vector3Int(x, y, z));
        }

        // There are 3 cases:
        // 1. This chunk never been loaded before.
        // 2. This chunk is still in file but memory.
        // 3. This chunk is loaded in memory.
        public ChunkHandle GetChunkData(Vector3Int chunkLoc)
        {
            if (chunks.TryGetValue(chunkLoc, out ChunkHandle handle))
            {
                if (handle.situation == ChunkHandle.Situation.InDisk)
                {
                    using var fs = File.OpenRead(LevelPath);
                    fs.Seek(handle.fileOffset.Value, SeekOrigin.Begin);
                    fs.Read(buffer, 0, CHUNK_STRIDE);
                    var location = Vector3Int.FromBytes(buffer, 0);
                    if (!location.Equals(chunkLoc)) throw new Exception("Wrong file offset");

                    byte[,,] data = new byte[16, 16, 16];
                    Buffer.BlockCopy(buffer, sizeof(int) * 3, data, 0, CHUNK_DATA_SIZE);
                    handle.data = data;
                }
                return handle.MakeRef();
            }
            else
            {
                chunks.Add(chunkLoc, new ChunkHandle(new byte[16, 16, 16]));
                return chunks[chunkLoc].MakeRef();
            }
        }

        public void RequireSaveChunks()
        {
            var iter = from chunk in chunks
                       where !chunk.Value.active && chunk.Value.situation != ChunkHandle.Situation.InDisk
                       select chunk;

            foreach (var chunk in iter)
            {
                LevelMessage message = new LevelMessage();
                message.type = LevelMessage.MessageType.WriteChunk;
                message.location = chunk.Key;
                message.chunk = chunk.Value;
                messageQueue.Enqueue(message);
            }
        }

        public void SaveChunks()
        {
            using var fs = File.OpenWrite(LevelPath);
            SaveChunks(fs);
        }

        private void SaveChunks(FileStream fs)
        {
            var iter = from chunk in chunks
                       where !chunk.Value.active && chunk.Value.situation != ChunkHandle.Situation.InDisk
                       select chunk;

            foreach (var chunk in iter)
            {
                if (chunk.Value.situation == ChunkHandle.Situation.Both)
                {
                    fs.Seek(chunk.Value.fileOffset.Value, SeekOrigin.Begin);
                }
                else
                {
                    fs.Seek(0, SeekOrigin.End);
                    chunk.Key.GetBytes(buffer);
                    chunk.Value.fileOffset = fs.Position;
                    fs.Write(buffer, 0, sizeof(int) * 3);
                }
                Buffer.BlockCopy(chunk.Value.data, 0, buffer, 0, CHUNK_DATA_SIZE);
                fs.Write(buffer, 0, CHUNK_DATA_SIZE);
                chunk.Value.data = null;
            }
        }

        private void WriteHeader(FileStream fs)
        {
            fs.Seek(LEVEL_HEADER_OFFSET, SeekOrigin.Begin);

            var name = Encoding.UTF8.GetBytes(levelName);
            var numChunks = BitConverter.GetBytes(chunks.Count);
            int len = name.Length + numChunks.Length;

            fs.Write(BitConverter.GetBytes(len));
            fs.Write(name);
            fs.Write(numChunks);
        }

        public void CloseLevel()
        {
            using FileStream fs = File.OpenWrite(LevelPath);
            SaveChunks(fs);
            WriteHeader(fs);
        }

        public void PrintLevelStatus()
        {
            foreach (var chunk in chunks)
            {
                if (chunk.Value.Saving) Console.WriteLine($"{chunk.Key}:{chunk.Value.situation}(Saving)");
                else Console.WriteLine($"{chunk.Key}:{chunk.Value.situation}");
            }
        }

        private void ClearState()
        {
            chunks.Clear();
            levelName = null;
        }

        public void OnGameClose()
        {
            threadStop = true;
            ioThread.Join();
        }
    }
}
